/* Copyright 2009 Anthony Surma
This file is part of Java's Kademlia Extension (JKADX).

JKADX is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

JKADX is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with JKADX.  If not, see <http://www.gnu.org/licenses/>.
 */

package jkadx.dht;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Anthony Surma
 */
public final class DHT
{        
    private   final static int           TYPE_SIZE_M1 = UniqueId.TYPE_SIZE_M1;
    private   final static int           NBITS        = UniqueId.NBITS;
    private   final static int           LENGTH       = UniqueId.LENGTH;
    public    final static String        SEPERATOR    = "&";
              final static int           K            = KBucket.K;
    private   final        KBucket[]     arrayOfKBuckets;
    private   final        DHTInterface  app;
              final        UniqueId      myUniqueId;
    private   volatile     boolean       areAtLeastK  = false;
    @Testing  final        AtomicInteger triedToGetKBucketForMyNodeIdCount = new AtomicInteger(0);

    /**
     * @param app The class which implements the DHTInterface interface.
     * @param mynodeid Unique ID reprenting this node on the network.
     */
    public DHT(final DHTInterface app, final UniqueId myNodeId)
    {
	this.myUniqueId = myNodeId;
	this.arrayOfKBuckets = new KBucket[NBITS];
	this.app = app;
	KBucket[] arrayOfKBuckets = this.arrayOfKBuckets;

	for (int i = 0; i < NBITS; i++)
	{
	    arrayOfKBuckets[i] = new KBucket(i);
	}
    }
   
    /**
     * @return A String of all known contacts in all K-Buckets.
     */
    @Override
    public String toString()
    {
	final StringBuilder result = new StringBuilder();	

	for (KBucket k : arrayOfKBuckets)
	{
	    if (!k.isEmpty)
	    {
		result.append(k);
                result.append(SEPERATOR);
	    }
	}
	return result.toString();
    }

    /**
     * @param thiscontact The contact to update if it's already in a K-Bucket, 
     * or add to a K-Bucket if it isn't.
     * @return true if the contact was added, updated, or would have been
     * updated if it weren't for the the kbucket being full.  Returns false only
     * if the node id was already known with different contact information, and
     * has responded to a ping.  Also will return false if trying to add a
     * contact with your own UniqueId.
     * <p>
     * Or, in other words, returns false if someone is trying to use an existing
     * node ID, or if you try to add your own node ID to a K-bucket.
     */
    public boolean updateOrAddNew(final Contact thiscontact)
    {
	// best effort made not to discard updates, will still
	// do so when there's no clear alternative.
	// getKClosestContactsTo.e. when kbucket is full and ALL contacts are currently locked
	// by other threads because it's waiting on isConnected() to return
	// for that contact.  It's reasonable to discard in this circumstance,
	// but perhaps we could make another list for each kbucket that holds
	// discarded new contacts?

	final KBucket kBucket = getKBucketFor(thiscontact);
	Contact existing;	

	if (kBucket != null)
	{
	    existing = kBucket.updateIfEqualMatchElseAddIfNotFull(thiscontact);
	    if (existing == null)
            {
		return true;
	    }
	    else
	    {
		if (app.isConnected(existing))
		{
		    kBucket.update(existing, thiscontact);
		}
		else // existing contact is not currently responding
		{
		    return kBucket.replace(existing, thiscontact);
		}
	    }
	}
        else /*For Testing Purposes */
	{
	    triedToGetKBucketForMyNodeIdCount.incrementAndGet();	   
	}
	return false;
    }

    boolean updateOrAddNew(final UniqueId nodeId, final byte[] ipAddress, final int port)
    {
	return updateOrAddNew(new Contact(nodeId, ipAddress, port));
    }

    /**
     * @param nodeid A UniqueId of total NBITS representing a node's unique ID.
     * @return An array of K known contacts that are closest to the target nodeid
     * sortedContacts by distance, or all known contacts if the total number of known
     * contacts is less than K.
     * <p>
     * Or, in other words, the only time this returns less than K contacts is when
     * there are less than K known contacts in all kbuckets.
     */
     public Contact[] getKClosestContactsTo(final UniqueId target)
    {
	final int           total = (areAtLeastK) ? K : this.getSizeUpToK();
	final Contact[]   results = new Contact[total];

	KBucket[] arrayOfKBuckets = this.arrayOfKBuckets;
        Contact[] sortedContacts;
        int found = 0;
        int needMore;
        int index = 0;
        int pos = 0;
        final int[] xord = new int[LENGTH];
        int j;
        int x;

        do
        {
            j = TYPE_SIZE_M1;
            x = (xord[index] = (myUniqueId.bits[index] ^ target.bits[index]));

            do
            {
                if (((x & 0x80000000) == 0x80000000) && !arrayOfKBuckets[pos].isEmpty)
                {
                    sortedContacts = arrayOfKBuckets[pos].sortByDistanceTo(target);
                    needMore = sortedContacts.length;
                    for (int k = 0; k < needMore; ++k)
                    {
                        results[found++] = sortedContacts[k];
                        if (found == total) return results;
                    }
                }
                x <<= 1;
                ++pos;
            } while (--j >= 0);
        } while (LENGTH > ++index);
        do // If haven't gotten total nodes yet, then getFrom some from clear bit buckets...
        {
            j = TYPE_SIZE_M1;
            x = xord[--index];

            do
            {
                if (!arrayOfKBuckets[--pos].isEmpty && (x & 1) == 0) // XXX take another look at this logic
                {
                    sortedContacts = arrayOfKBuckets[pos].sortByDistanceTo(target);
                    needMore = sortedContacts.length;
                    for (int k = 0; k < needMore; ++k)
                    {
                        results[found++] = sortedContacts[k]; // caught an array indexoutofboundsexception: 0 here
                        if (found == total) return results;
                    }
                }
                x >>>= 1;
            } while (--j >= 0);
        } while (index > 0); // XXX probably don't need a conditional check here
        return results;
    }

   /**
    * @return The total number of known contacts.
    */
    public int size()
    {
	int size = 0;
	
	for (KBucket k : arrayOfKBuckets)
	{
	    size += k.getSize();
	}
	return size;
    }
    
    private KBucket getKBucketFor(final Contact thisContact)
    {	
	final int index = UniqueId.getMSBAfterXoring(thisContact.uniqueId, myUniqueId);

	if (index >= 0)	
	    return arrayOfKBuckets[index];	
	return null;
    }
    
    private int getSizeUpToK()
    {
	int total = 0;
	
	for (KBucket k : arrayOfKBuckets)
	{
            if ((total += k.getSize()) >= K)
            {
                areAtLeastK = true;
                return K;
            }
	}
	return total;
    }

    public UniqueId getRandomUniqueIdFromKBucket(final int position)
    {
        final int[] result = UniqueId.getRandomInts();
              int   index  = position >>> 5;
        final int   offset = position & 31;
              int   temp1  = result[index];
              int   temp2  = myUniqueId.bits[index];
              int   temp3  = ~temp2;

        temp1 <<= (offset + 1);
        temp1 >>>= (offset + 1);

        temp3 >>>= (31 - offset);
        temp3 <<= (31 - offset);

        temp1 |= temp3;

        if (offset > 0)
        {
            temp1 <<= offset;
            temp1 >>>= offset;
        
            temp2 >>>= 32 - offset;
            temp2 <<= 32 - offset;
            temp1 |= temp2;
        }
        result[index] = temp1;
        while (--index >= 0)
        {
            result[index] = myUniqueId.bits[index];
        }
 
        
       
        return new UniqueId(result, true);
    }

    @Testing int getSizeTest()
    {	
	int total = 0;

	for (KBucket k : arrayOfKBuckets)
	{
	    total += k.getSizeTest();
	}
	return total;
    }

    @Testing boolean isFull()
    {	
	for (KBucket k : arrayOfKBuckets)
	{
	    if (!k.isFull())	    
		return false;	    
	}
	return true;
    }

    @Testing int getAndResetDiscardedCount()
    {
	return KBucket.discarded.getAndSet(0);
    }

    @Testing int getAndResetUpdatedWithoutCheckingConnectionCount() // purely for testing
    {
	return KBucket.updated.getAndSet(0);
    }

    @Testing int getAndResetTriedToGetKBucketForMyNodeIdCount() // purely for testing
    {
	return triedToGetKBucketForMyNodeIdCount.getAndSet(0);
    }

    @Testing int getAndResetAddedEqualContactToKBucket()
    {
        return KBucket.addedEqual.getAndSet(0);
    }

    @Testing int getNumLockedContacts()
    {
        int num = 0;
        for (KBucket k : this.arrayOfKBuckets)
        {
            num += k.getNumContactsLocked();
        }
        return num;
    }

    @Testing int getSizeOfReplaceListsTest()
    {
        int num = 0;
        for (KBucket k : this.arrayOfKBuckets)
        {
            num += k.getSizeOfReplaceListTest();
        }
        return num;
    }
}

// <editor-fold defaultstate="collapsed" desc="@Testing: Only used in testing.">
@interface Testing {
}// </editor-fold>

